package com.googlecode.wheres_my_stuff_okay_potato;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.util.Hashtable;

import android.content.Context;
import android.util.Log;

/**
 * Utility class that handles user logins and user account creation.
 * 
 * @author Chad Miller
 * @since 2/21/2013
 */
public class LoginHandler {
	private static final String ASSETS_DIR = "assets";
	private static final String DB_FILE = "users.db";
	private static final String LH_TAG = "lh";
	private final Context context;

	/** Hashtable of usernames and user instances. */
	private Hashtable<String, User> users;
	private byte attemptsLeft;

	/**
	 * Create a login handler. Normally, users should be
	 * <code>new UserIO.readFile()</code>, but this is not done for the sake of
	 * programmer freedom.
	 * 
	 * @param users
	 *            is the user database this should be created with.
	 */
	public LoginHandler(Context context) {
		users = new Hashtable<String, User>();
		attemptsLeft = 3;
		this.context = context;

		try {
			loadDatabase();
			createDefaultAdmin();
		} catch (FileNotFoundException e) {
			try {
				saveDatabase();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Tries to log in a user using a username and a password. The hashtable
	 * takes in the username string and compares its associated password with
	 * the password argument for this method. If both the username exists and
	 * the correct password was given, true will be returned.
	 * 
	 * @param username
	 *            is the user's username.
	 * @param password
	 *            is the user's password.
	 * @throws User.LockedException
	 *             if the user's account is locked.
	 * @throws User.WrongPasswordException
	 *             if the wrong password was entered.
	 * @throws UserNotFoundException
	 *             if a user with the specified username does not exist.
	 * @return true if the correct username/password combination was entered;
	 *         false otherwise.
	 */
	public void attemptLogin(String username, String password)
			throws User.LockedException, User.WrongPasswordException,
			UserNotFoundException {
		if (userExists(username)) {
			User user = users.get(username);
			Log.d(LH_TAG, "About to attempt login for " + username
					+ " with password " + password);

			try {
				user.attemptLogin(password);
			} catch (User.WrongPasswordException pwe) {
				attemptsLeft--;

				if (attemptsLeft <= 0) {
					attemptsLeft = 3;
					user.lock();
					users.put(username, user);
					throw user.new LockedException();
				} else
					throw pwe;
			}
		} else
			throw new UserNotFoundException();
	}

	/**
	 * Checks if a username exists. Simply takes in a username String and
	 * returns whether or not the username exists in the username database.
	 * 
	 * @param username
	 *            is the username to be checked for existence.
	 * @return true if the username exists in the database; false otherwise.
	 */
	public boolean userExists(String username) {
		return users.containsKey(username);
	}

	/**
	 * Creates a new user. Takes in a username and password string and adds them
	 * to the database. The username and password are entered as a key/value
	 * pair, respectively.
	 * 
	 * @param username
	 *            is the username to create a new user with.
	 * @param password
	 *            is the password to create a new user with.
	 */
	public boolean createUser(String username, String password, String email) {
		if (!userExists(username)) {
			users.put(username, new User(username, password, email));
			return true;
		} else
			return false;
	}

	/**
	 * Removes a user from the database.
	 * 
	 * @param username
	 *            is the username of the user that should be deleted.
	 * @return true if the user was deleted; false otherwise.
	 */
	public boolean deleteUser(String username) {
		if (userExists(username)) {
			users.remove(username); // removes user from hash table
			try {
				saveDatabase();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
			return true;
		} else
			return false;
	}

	/**
	 * Creates an admin the same way a user is created.
	 * 
	 * @param username
	 *            is the username the admin should be created with.
	 * @param password
	 *            is the password the admin should be created with.
	 * @param email
	 *            is the email address the admin should be created with.
	 * @return true if an admin with the name does not exist and was
	 *         successfully created; false otherwise.
	 */
	public boolean createAdmin(String username, String password, String email) {
		if (!userExists(username)) {
			users.put(username, new Admin(username, password, email));
			return true;
		} else
			return false;
	}

	/**
	 * Creates the default administrator with the following parameters: Username
	 * = Burrito Password = Burrito Email address = breetz@burrito.br
	 */
	public void createDefaultAdmin() {
		if (!userExists("Burrito")) {
			users.put("Burrito", new Admin("Burrito", "Burrito",
					"breetz@burrito.br"));
		}
		if (!userExists("burrito")) {
			users.put("burrito", new User("burrito", "burrito",
					"breetz@burrito.br"));
		}
	}

	/**
	 * Returns a user with a specified username from the database.
	 * 
	 * @param username
	 *            is the username of the user that should be returned.
	 * @return the user with the associated username, or null if it does not
	 *         exist.
	 */
	public User getUser(String username) {
		if (userExists(username))
			return users.get(username);
		else
			return null;
	}

	/**
	 * This method will unlock a locked user account.
	 * 
	 * @param username
	 *            The username of the user to be unlocked
	 * @return true if the user is successfully unlocked
	 */
	public boolean unlockUser(String username) throws UserNotFoundException {
		if (userExists(username)) {
			users.get(username).unlock();
			try {
				saveDatabase();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
			return true;
		} else
			throw new UserNotFoundException();
	}

	/**
	 * Writes the user database file to the assets folder. Essentially, all this
	 * does is write a hashtable - the only argument this method uses - to a
	 * file.
	 */
	public void saveDatabase() throws FileNotFoundException, IOException {
		final File dir = new File(context.getFilesDir() + File.separator
				+ ASSETS_DIR);
		dir.mkdirs();
		final File file = new File(dir, DB_FILE);

		ObjectOutputStream output = new ObjectOutputStream(
				new FileOutputStream(file));
		output.writeObject(users);
		output.close();
	}

	/**
	 * Reads a user database file. There is only one and its location will
	 * always remain constant, so this takes no parameters.
	 */
	@SuppressWarnings("unchecked")
	public void loadDatabase() throws StreamCorruptedException,
			FileNotFoundException, IOException, ClassNotFoundException {
		final File dir = new File(context.getFilesDir() + File.separator
				+ ASSETS_DIR);

		ObjectInputStream input = new ObjectInputStream(new FileInputStream(
				new File(dir, DB_FILE)));
		users = (Hashtable<String, User>) input.readObject();
		input.close();
	}

	/**
	 * Simple exception thrown when a specified user is not found in the
	 * database.
	 * 
	 * @author Jeremy Hehn
	 */
	public class UserNotFoundException extends Exception {
		private static final long serialVersionUID = 2023735120383915398L;

		/**
		 * Creates a new UserNotFoundException and logs a message.
		 */
		public UserNotFoundException() {
			super();
			Log.d(LH_TAG, "User not found!");
		}

		/**
		 * Creates a new UserNotFoundException with a specific message. A
		 * message is logged, but not the message this exception was
		 * instantiated with.
		 * 
		 * @param message
		 *            is a specific error message that should be used.
		 */
		public UserNotFoundException(String message) {
			super(message);
			Log.d(LH_TAG, "User not found!");
		}
	}
}